package cnt5106.p2p.common.protocol;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Vector;

import cnt5106.p2p.common.Utils;

public class Message {

	public static Message parse(byte[] data) {
		Message message = new Message();
		
		// Parse type
		message.type = MessageType.getMessageType(data[0]);
		
		// Parse filename
		int filenameLength = Utils.convertByteArrayToInt(Arrays.copyOfRange(data, 1, 5));
		if (filenameLength != 0)
			message.filename = new String(Arrays.copyOfRange(data, 5, 5 + filenameLength));
		
		// Parse number
		message.number = Utils.convertByteArrayToInt(Arrays.copyOfRange(data, 5 + filenameLength, 9 + filenameLength));
		
		// Parse sequential no
		message.seqNo = Utils.convertByteArrayToInt(Arrays.copyOfRange(data, 9 + filenameLength, 13 + filenameLength));
				
		// Parse data (skip the data length field)
		if (data.length > 17 + filenameLength)
			message.data = Arrays.copyOfRange(data, 17 + filenameLength, data.length);
		
		return message;
	}
	
	public static Message generate(MessageType type, String filename, int number, Object data) {
		return generate(type, filename, number, 0, data);
	}
	
	public static Message generate(MessageType type, String filename, int number, int seqNo, Object data) {
		Message message = new Message();
		message.type = type;
		message.filename = filename;
		message.number = number;
		message.seqNo = seqNo;
		message.setData(data);
		return message;
	}
	
	private Message() {
		
	}
	
	/**
	 * Make the whole message into bytes for transmission.
	 * @return the byte array of the message
	 */
	public byte[] toByte() {
		Vector<Byte> byteVector = new Vector<Byte>();
		
		// Generate type field
		byteVector.add(type.getCode());
		
		// Generate filename length field
		Utils.appendArrayToVector(byteVector, Utils.convertIntToByteArray(filename == null ? 0 : filename.length()));
		
		// Generate filename field
		if (filename != null) Utils.appendArrayToVector(byteVector, filename.getBytes());
		
		// Generate number field
		Utils.appendArrayToVector(byteVector, Utils.convertIntToByteArray(number));
		
		// Generate seqNo field
		Utils.appendArrayToVector(byteVector, Utils.convertIntToByteArray(seqNo));
		
		// Generate data length field
		Utils.appendArrayToVector(byteVector, Utils.convertIntToByteArray((data == null) ? 0 : data.length));
		
		// Generate data field
		if (data != null) Utils.appendArrayToVector(byteVector, data);
		
		return Utils.unboxByteArray((Byte[]) byteVector.toArray(new Byte[0]));
	}

	private void setData(Object data) {
		if (data == null) return;
		if (data instanceof InetSocketAddress) {
			this.data = new byte[6];
			for (int i = 0; i != 4; ++i) {
				this.data[i] = ((InetSocketAddress) data).getAddress().getAddress()[i]; 
			}
			this.data[4] = (byte) (((InetSocketAddress) data).getPort() >> 8 & 0xFF);
			this.data[5] = (byte) (((InetSocketAddress) data).getPort() & 0xFF);
		} else if (data instanceof Vector<?> && ((Vector<?>)data).size() != 0 && ((Vector<?>)data).get(0) instanceof InetSocketAddress) {
			this.data = new byte[6 * ((Vector<?>) data).size()];
			@SuppressWarnings("unchecked")
			Vector<InetSocketAddress> addresses = (Vector<InetSocketAddress>) data;
			for (int i = 0; i != addresses.size(); ++i) {
				for (int j = 0; j != 4; ++j) {
					this.data[i * 6 + j] = addresses.get(i).getAddress().getAddress()[j]; 
				}
				this.data[i * 6 + 4] = (byte) (addresses.get(i).getPort() >> 8);
				this.data[i * 6 + 5] = (byte) addresses.get(i).getPort();
			}
		} else if (data instanceof Vector<?> && ((Vector<?>)data).size() != 0 && ((Vector<?>)data).get(0) instanceof String) {
			@SuppressWarnings("unchecked")
			Vector<String> filenames = (Vector<String>) data;
			String dataString = new String();
			for (String filename : filenames) {
				dataString = dataString + filename + "\0";
			}
			this.data = dataString.getBytes();
		} else if (data instanceof byte[]) {
			this.data = (byte[]) data;
		}
	}
	
	/**
	 * Parse and return the data part in specific type of object
	 * @return the object that represents the data
	 */
	public Object getData() {
		switch (type) {
		case TRACKER_REGISTER_PEER:
		case TRACKER_REGISTER_GROUP:
		case TRACKER_NOTIFY_NEW_PEER:
		case PEER_TCPPORT_REPLY:
		case PEER_FINISH:
			try {
				return new InetSocketAddress(InetAddress.getByAddress(Arrays.copyOfRange(data, 0, 4)), ((data[4] & 0x000000FF) << 8) | (data[5] & 0x000000FF));
			} catch (UnknownHostException e) {
				// Never throw
				e.printStackTrace();
			}
			break;
		case TRACKER_LIST:
		case TRACKER_REPLY_PEER:
		case PEER_REQUEST_CHUNK:
		case PEER_NEGATIVE_REPLY:
		case PEER_FILEOWNER_END:
		case PEER_TCPPORT_REQUEST:
		case TRACKER_DUPLICATE_FILENAME:
		case TRACKER_FILE_NOT_FOUND:
		case PEER_DATA_ACK:
			return null;
		case TRACKER_REPLY_GROUP:
			Vector<InetSocketAddress> addresses = new Vector<InetSocketAddress>();
			for (int i = 0; i != data.length / 6; ++i) {
				try {
					addresses.add(new InetSocketAddress(
							InetAddress.getByAddress(
									Arrays.copyOfRange(data, i * 6, i * 6 + 4)), 
									((data[i * 6 + 4] & 0x000000FF) << 8) | (data[i * 6 + 5] & 0x000000FF)));
				} catch (UnknownHostException e) {
					// Never throw
					e.printStackTrace();
				}
			}
			return addresses; 
		case TRACKER_REPLY_LIST:
			int lastPos = 0;
			Vector<String> filenames = new Vector<String>();
			if (data != null)
				for (int i = 0; i != data.length; ++i) {
					if (data[i] == 0) {
						filenames.add(new String(Arrays.copyOfRange(data, lastPos, i)));
						lastPos = i + 1;
					}
				}
			return filenames;
		case PEER_POSITIVE_REPLY:
		case PEER_FILEOWNER_DATAPACKET:
			return data;
		}
		return null;
	}
	
	public static boolean verifyMessageCompletion(byte[] msg) {
		int length = 17;
		if (msg.length < length) return false;
		length += Utils.convertByteArrayToInt(Arrays.copyOfRange(msg, 1, 5));
		if (msg.length < length) return false;
		length += Utils.convertByteArrayToInt(Arrays.copyOfRange(msg, length - 4, length));
		if (msg.length < length) return false;
		return true;
	}
	
	public String getFilename() {
		return filename;
	}

	public MessageType getType() {
		return type;
	}

	public int getNumber() {
		return number;
	}

	public int getSeqNo() {
		return seqNo;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Message)) return false;
		else 
			return this.type == ((Message) obj).type
				&& ((this.filename == null && ((Message) obj).filename == null) || this.filename.equals(((Message) obj).filename))
				&& this.number == ((Message) obj).number
				&& Arrays.equals(this.data, ((Message) obj).data)
				&& this.seqNo == ((Message) obj).seqNo;
	}
	
	@Override
	public String toString() {
		return "[" + type.toString() + "][" + filename + "][" + number + "][" + seqNo + "]";
	}

	private String filename = null;
	private MessageType type = MessageType.UNKNOWN_MESSAGE_TYPE;
	private byte[] data = null;
	private int number = 0;
	private int seqNo = 0;
}
